'''Copyright 2011 Google Inc. All Rights Reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
#limitations under the License.'''

from client import db_api
from client import rebelcommand

from google.appengine.api import app_identity
from google.appengine.api import modules
from google.appengine.api import oauth
from google.appengine.api import urlfetch
from google.appengine.api import users

from google.appengine.api.urlfetch import fetch

import cgi
import datetime
import json
import logging
import os
import pprint as pp
import random
import sys
import traceback
import urllib
import webapp2


# constants
_DEBUG = True
_JSON_ENCODER = json.JSONEncoder()

if _DEBUG:
  _JSON_ENCODER.indent = 4
  _JSON_ENCODER.sort_keys = True

module_name = modules.get_current_module_name()
_dispatcher = rebelcommand.MissionDispatcher()

_EMAIL_SCOPE = "https://www.googleapis.com/auth/userinfo.email"
_IS_DEVELOPMENT = os.environ['SERVER_SOFTWARE'].startswith('Development/')

#######################################################################
# common functions
#######################################################################

def tojson(python_object):
  """Helper function to output and optionally pretty print JSON."""
  return _JSON_ENCODER.encode(python_object)


def fromjson(msg):
  """Helper function to ingest JSON."""
  try:
    return json.loads(msg)
  except Exception, e:
    raise Exception('Unable to parse as JSON: %s' % msg)

_PAIRING_KEY = fromjson(open('shared/pairing-key.json').read())['key']

def fetchjson(url, deadline, payload=None):
  """Fetch a remote JSON payload."""

  method = "GET"
  headers = {}
  if payload:
    method = "POST"
    headers['Content-Type'] = 'application/json'
  result = fetch(url, method=method, payload=payload, headers=headers, deadline=deadline).content
  return fromjson(result)


def json_by_default_dispatcher(router, request, response):
  """WSGI router which defaults to 'application/json'."""

  response.content_type = 'application/json'
  return router.default_dispatcher(request, response)


def e(msg):
  """Convient method to raise an exception."""

  raise Exception(repr(msg))


def w(msg):
  """Log a warning message."""

  logging.warning('##### %s' % repr(msg))





#######################################################################
# 'controller' handlers
#######################################################################


class JsonHandler(webapp2.RequestHandler):
  """Convenience class for handling JSON requests."""

  def handle(self, params, *args):
    raise Exception('subclasses must implement this method')

  def post(self, *args):
    logging.info('%s <- %s' % (self.request.path, self.request.body))
    try:
      if not self.request.body:
        raise Exception('Empty request')
      params = fromjson(self.request.body)
      r = self.handle(params, *args)
      if not r:
        raise Exception('Unexpected empty response from subclass')
      self.response.write(tojson(r))
    except:
      # clients must already be prepared to deal with non-JSON responses,
      # so a raw, human readable, stack trace is fine here
      self.response.set_status(500)
      tb = traceback.format_exc()
      logging.warn(tb)
      self.response.write(tb)


class FindGame(JsonHandler):

  def handle(self, params, userID):
    pingAll()
    return self.get_game(userID)

  def get_game(self, userID):
    # previously matched / player re-entering?
    player_game = _dispatcher.lookup_player_game(userID)
    if player_game:
      return player_game

    # look for a new available game
    result = self._get_player_game(userID)

    # found a game?
    if result and 'game' in result:
      player_game = result
      usr = db_api.getUser(userID)
      addPlayers(player_game, userID, usr.displayName)
      return result

    # more players needed to start game?
    if result.get('players_needed_for_next_game', None) > 0:
      return result

    # start a new game
    game = startGame()
    if 'game_state' not in game:
      logging.info('RETURNING RESULT FROM startGame(): %s' % game)
      return game

    logging.info('RETURNING RESULT: %s' % result)
    return result

  def _get_player_game(self, userID):
    player_game = _dispatcher.find_player_game(userID)
    return player_game


class UpdateGameState(JsonHandler):

  def handle(self, params, *args):
    serverid = params['serverid']
    new_game_state = params['game_state']
    game_name = new_game_state['name']
    _dispatcher.update_player_names(serverid, game_name, new_game_state)
    return {'success': True,
            'controller': module_name}


class RegisterMissionController(JsonHandler):

  def handle(self, params, *args):
    if _PAIRING_KEY != params['pairing_key']:
      logging.log ("controller.py: pairing keys not equal")
      return {'success': False,
              'exception': 'bad pairing key'}
    mission_controller_port = params['mission_controller_port']
    ip = self.request.remote_addr
    mission_controller_host = '%s:%d' % (ip, mission_controller_port)
    params['mission_controller_host'] = mission_controller_host
    _dispatcher.update_server_info(params)
    return {'success': True,
            'module': module_name,
            'mission_controller_host': mission_controller_host}


class ListGames(webapp2.RequestHandler):

  def get(self):
    self.response.write(tojson(pingAll()))


class Debug(webapp2.RequestHandler):

  def get(self):
    state = _dispatcher.get_state()
    self.response.write(tojson(state))


class StartGame(webapp2.RequestHandler):

  def get(self):
    self.response.write(tojson(startGame()))


class LogFiles(webapp2.RequestHandler):

  def get(self):
    self.response.headers['Content-Type'] = 'text/html'
    self.response.write('<html><body><h1>Log Files</h1>')
    for serverid in _dispatcher.get_game_servers():
      server_info = _dispatcher.get_game_server_info(serverid)
      self.response.write('<p>%(id)s: '
                          '<a href="http://%(svr)s/forever.log">error</a> '
                          '<a href="http://%(svr)s/log">console</a> '
                          '<a href="http://%(svr)s/ping">ping</a> '
                          '<a href="http://%(svr)s/enable-dedup">enable-dedup</a> '
                          '<a href="http://%(svr)s/disable-dedup">disable-dedup</a> '
                          '</p>' %
                          {'id':serverid,
                           'svr':server_info['controller_host']})
    self.response.write('</body></html>')


class GameOver(JsonHandler):

  def handle(self, params, *args):
    _dispatcher.del_game(params['serverid'], params['name'])
    return {'success': True, 'controller': module_name}


def pingAll():
  removeserver = []
  # TODO use async urlfetch to parallelize url fetch calls
  for serverid in _dispatcher.get_game_servers():
    server_info = _dispatcher.get_game_server_info(serverid)
    url = 'http://%s/ping' % server_info['mission_controller_host']
    try:
      r = fetchjson(url, 2)
      logging.debug('pingAll(): %s -> %s' % (url, r))
    except:
      logging.warn('pingAll(): EXCEPTION %s' % traceback.format_exc())
      removeserver.append(serverid)
    if r['serverid'] != serverid:
      removeserver.append(serverid)
      continue

    # check for games which have ended without our knowledge
    remotegameinfo = r['gameinfo']
    server_struct = _dispatcher.get_game_server_struct(serverid)
    games = server_struct['games']
    removegame = []
    for name, game in games.iteritems():
      if name not in remotegameinfo:
        # the game is unexpectedly gone
        logging.warn('serverid %s unexpectedly lost game %s (did we miss a /game-over callback?)' % (serverid, name))
        removegame.append(name)
    for name in removegame:
      _dispatcher.del_game(serverid, name)

  for serverid in removeserver:
    _dispatcher.del_game_server(serverid)
  return _dispatcher.get_game_servers()


# helper function
def rateUsage(r):
  return max(int(r['cpu']), int(r['mem']))


# helper function
def startGame():
  logging.debug('startGame()')
  best = None
  bestServer = None
  for serverid, server_struct in pingAll().iteritems():
    if not best or rateUsage(best) > rateUsage(r):
      server_info = server_struct['server_info']
      best = server_info
      bestServer = best['mission_controller_host']
  if bestServer:
    url = 'http://%s/start-game?p=%s' % (bestServer, _PAIRING_KEY)
    game = fetchjson(url, 20)
    logging.debug('startGame(): %s -> %s' % (url, tojson(game)))
    if game.get('success', False):
      ip = bestServer.split(':')[0]
      game['gameURL'] = 'http://%s:%s/%s' % (ip, game['port'], game['name'])
      game['mission_controller_host'] = bestServer
      game['serverid'] = best['serverid']
      _dispatcher.update_game(game)
    return game
  else:
    return {'result': 'no-available-servers'}


def addPlayers(player_game, userID, displayName):
  logging.info('addPlayers(player_game=%s, userID=%s, displayName=%s)' % (player_game, userID, displayName))
  game = player_game['game']
  url = 'http://%s/add-players?p=%s' % (game['controller_host'], _PAIRING_KEY)
  player_game_key = player_game['player_game_key']
  msg = {
    'userID': userID,
    'displayName': displayName,
    'game_name' : game['name'],
    'player_game_key': player_game_key,
  }
  try:
    result = fetchjson(url, 20, payload=tojson(msg))
    logging.info('addPlayers(): %s -> %s -> %s' % (tojson(msg), url, tojson(result)))
    return {
      'game': game,
      'player_game_key': player_game_key,
      'success': True,
    }
  except:
    exc = traceback.format_exc()
    logging.info('addPlayers(): %s -> %s -> %s' % (tojson(msg), url, exc))
    return {
      'success' : False,
      'exception': exc,
    }




#######################################################################




# handler common to frontends and backends
handlers = []
logging.info("main.py: Configuring handlers")
logging.info("main.py: For Module:" + str(module_name) )
handlers.extend([
  ('/controller/find-game/(.*)', FindGame),
  ('/controller/update-game-state', UpdateGameState),
  ('/controller/register-mission-controller', RegisterMissionController),
  ('/controller/list-games', ListGames),
  ('/controller/debug', Debug),
  ('/controller/start-game', StartGame),
  ('/controller/game-over', GameOver),
  ('/controller/log-files', LogFiles),
])

app = webapp2.WSGIApplication(handlers, debug=True)
app.router.set_dispatcher(json_by_default_dispatcher)
